package server.rest.sessions;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class SessionManager<KEY, ID, SESSION extends Session<KEY, ID>> implements Runnable
{
    private LinkedList<HashMap<KEY, SESSION>> sessionList;
    private HashMap<KEY, HashMap<KEY, SESSION>> maps;
    private Expiration<KEY, ID, SESSION> expiration;
    private HashMap<KEY, SESSION> recentlyExpired;

    final int timeoutMs;
    ScheduledExecutorService scheduledService;

    public SessionManager(int timeoutMs, int buffers) throws Exception
    {
        this(timeoutMs, buffers, null);
    }

    public SessionManager(int timeoutMs, int buffers, Expiration<KEY, ID, SESSION> expiration) throws Exception
    {
        if (buffers < 2)
        {
            throw new Exception("There must be at least two buffers. buffers=" + buffers);
        }
        this.maps = new HashMap<KEY, HashMap<KEY, SESSION>>();
        this.sessionList = new LinkedList<HashMap<KEY, SESSION>>();
        for (int i = 0; i < buffers; i++)
        {
            this.sessionList.add(new HashMap<KEY, SESSION>());
        }
        this.expiration = expiration;
        this.timeoutMs = timeoutMs;

        long periodMs = timeoutMs / (buffers - 1);

        scheduledService = Executors.newScheduledThreadPool(buffers * 2);

        scheduledService.scheduleWithFixedDelay(this, periodMs, periodMs, TimeUnit.MILLISECONDS);
    }

    synchronized public SESSION findAndRefresh(KEY key)
    {
        HashMap<KEY, SESSION> map = this.maps.get(key);
        if (map == null)
        {
            return null;
        }
        SESSION session = map.get(key);

        HashMap<KEY, SESSION> first = sessionList.getFirst();
        if (first != map)
        {
            map.remove(key);
            first.put(key, session);
            this.maps.put(key, first);
        }
        session.refresh();
        return session;

    }

    synchronized public void add(SESSION session)
    {
        KEY key = session.getKey();
        _remove(key);
        HashMap<KEY, SESSION> first = sessionList.getFirst();
        this.maps.put(key, first);
        first.put(key, session);
    }

    synchronized public boolean remove(KEY key)
    {
        return _remove(key);
    }

    synchronized public boolean remove(Session<KEY, ID> session)
    {
        return _remove(session.getKey());
    }

    private boolean _remove(KEY key)
    {
        HashMap<KEY, SESSION> map = this.maps.get(key);
        if (map == null)
        {
            return false;
        }
        map.remove(key);
        this.maps.remove(key);
        return true;
    }

    public Session<KEY, ID> getSession(KEY key)
    {
        Session<KEY, ID> session = findAndRefresh(key);
        if (session == null)
        {
            return null;
        }
        return session;
    }

    public void run()
    {
        HashMap<KEY, SESSION> expired = null;
        synchronized (this)
        {
            expired = this.sessionList.getLast();
            if (expired.size() > 0)
            {
                this.recentlyExpired = expired;
                System.out.println(this.recentlyExpired.keySet());
                System.out.println(this.recentlyExpired.size());
            }
            this.sessionList.removeLast();
            this.sessionList.addFirst(new HashMap<KEY, SESSION>());
            for (KEY key : expired.keySet())
            {
                this.maps.remove(key);
            }
        }
        if ((this.expiration != null) && (expired.size() > 0))
        {
            this.expiration.expire(expired.values());
        }
    }

    public int getTimeoutMs()
    {
        return this.timeoutMs;
    }

    public HashMap<KEY, SESSION> getRecentlyExpired()
    {
        return recentlyExpired;
    }

    public void setExpiration(Expiration<KEY, ID, SESSION> expiration)
    {
        this.expiration = expiration;
    }
}
